package site.hanzhe.did.utils;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import site.hanzhe.did.entity.DockerImage;
import site.hanzhe.did.entity.DockerManifest;
import site.hanzhe.did.entity.Layer;
import site.hanzhe.did.entity.Platform;
import site.hanzhe.did.props.DockerProp;

import java.io.*;
import java.net.Proxy;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.zip.GZIPInputStream;

/**
 * HTTP请求工具类
 *
 * @author 张涵哲
 * @since 2024-06-12 21:03:59
 */
public class RequestUtil {

    private static String token;
    private static OkHttpClient client;

    private static OkHttpClient getClient() {
        if (client == null) {
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            Proxy proxy = ConfigUtil.getProxy();
            if (proxy != null) {
                builder.proxy(proxy);
            }
            client = builder.build();
        }
        return client;
    }

    /**
     * 初始化授权信息(可能是第三方镜像仓库)
     *
     * @author 张涵哲
     * @since 2024-06-12 22:00:21
     */
    public static void initAuthDetails() {
        // Console.log("初始化授权信息....");
        // 请求仓库接口，可能是Docker官方，也可能是第三方仓库
        String url = "https://" + DockerProp.registry + "/v2/";
        Request build = new Request.Builder().url(url).get().build();
        try (Response response = getClient().newCall(build).execute()) {
            if (response.code() == HttpStatus.HTTP_UNAUTHORIZED) {
                // 取出这个仓库的授权地址，不同仓库的授权地址不同
                String wwwAuthenticate = response.header("WWW-Authenticate");
                String[] parts = wwwAuthenticate.split("\"");
                if (parts.length > 1) {
                    DockerProp.authUrl = parts[1];
                    if (parts.length > 3) {
                        DockerProp.regService = parts[3];
                    }
                }
            }
        } catch (Exception e) {
            Console.log("初始化授权信息失败！");
            throw new RuntimeException(e);
        }

        // Console.log("authUrl: {}", authUrl);
        // Console.log("regService: {}", regService);
    }

    /**
     * 获取请求Token
     *
     * @author 张涵哲
     * @since 2024-06-12 23:02:11
     */
    private static String getToken() {
        if (StrUtil.isBlank(token)) {
            // Console.log("正在获取Token...");
            String mediaType = "application/vnd.docker.distribution.manifest.list.v2+json";
            String url = DockerProp.authUrl + "?service=" + DockerProp.regService + "&scope=repository:" + DockerProp.repository + "/" + DockerProp.img + ":pull";
            Request build = new Request.Builder().url(url).get().build();
            // Console.log("请求地址：{}", url);
            try (Response response = getClient().newCall(build).execute()) {
                String jsonStr = response.body().string();
                JSON jsonObj = JSONUtil.parse(jsonStr);
                token = "Bearer " + jsonObj.getByPath("token", String.class);
                // Console.log("Token获取成功：{}", token);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return token;
    }

    /**
     * 发起GET请求
     *
     * @author 张涵哲
     * @since 2024-06-12 23:05:50
     */
    private static Response get(String url, String mediaType) {
        // Console.log("请求地址：{}", url);
        Request build = new Request.Builder().url(url)
                .header("Authorization", getToken())
                .header("Accept", mediaType)
                .build();
        try {
            Response response = getClient().newCall(build).execute();
            if (response.code() != HttpStatus.HTTP_OK) {
                String string = response.body().string();
                response.close();
                throw new RuntimeException("请求失败！响应信息为：" + string);
            }
            return response;
        } catch (IOException e) {
            Console.log("get请求失败：" + url);
            throw new RuntimeException(e);
        }
    }

    /* ----------------------------------------------------------------------------------------------------------------------------- */

    /**
     * 获取Docker清单信息
     *
     * @author 张涵哲
     * @since 2024-06-13 12:51:55
     */
    public static DockerManifest getDockerManifest() {
        // Console.log("正在获取Docker清单信息");
        String mediaType = "application/vnd.docker.distribution.manifest.list.v2+json";
        String requestUrl = "https://" + DockerProp.registry + "/v2/" + DockerProp.repository + "/" + DockerProp.img + "/manifests/" + DockerProp.tag;
        try (Response response = get(requestUrl, mediaType)) {
            String responseBody = response.body().string();
            return DockerManifest.parse(responseBody);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取Docker镜像信息
     *
     * @author 张涵哲
     * @since 2024-06-14 20:26:50
     */
    public static DockerImage getDockerImage(Platform platform) {
        // Console.log("正在获取Docker镜像信息");
        String mediaType = "application/vnd.oci.image.manifest.v1+json";
        String requestUrl = "https://" + DockerProp.registry + "/v2/" + DockerProp.repository + "/" + DockerProp.img + "/manifests/" + platform.getDigest();
        try (Response response = get(requestUrl, mediaType)) {
            String responseBody = response.body().string();
            return DockerImage.parse(responseBody);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 下载镜像摘要文件，docker inspect 看到的就是这个文件
     *
     * @author 张涵哲
     * @since 2024-06-13 13:32:58
     */
    public static void downloadConfig(DockerImage dockerImage) {
        // Console.log("正在下载镜像摘要文件");
        String digest = dockerImage.getDigest();
        String fileName = digest.substring(7) + ".json";
        String mediaType = "application/vnd.docker.distribution.manifest.list.v2+json";
        String requestUrl = "https://" + DockerProp.registry + "/v2/" + DockerProp.repository + "/" + DockerProp.img + "/blobs/" + digest;
        try (Response response = get(requestUrl, mediaType)) {
            byte[] bytes = response.body().bytes();
            FileUtil.writeBytes(bytes, DockerProp.workspace.getAbsolutePath() + "/" + fileName);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 解析并下载每一层layer
     *
     * @author 张涵哲
     * @since 2024-06-13 22:35:27
     */
    public static void downloadLayers(DockerImage dockerImage) {
        String parentId = "";
        // 创建layers目录，用于存储layer.tar文件
        List<Layer> layers = dockerImage.getLayers();
        String layerDir = DockerProp.workspace.getAbsolutePath() + "/layers";
        new File(layerDir).mkdir();
        Console.log("共找到{}个分层，准备开始下载", layers.size());
        // 遍历每一层layer进行下载
        for (int index = 0; index < layers.size(); index++) {
            Layer layer = layers.get(index);
            String digest = layer.getDigest();
            String fakeLayerId = generateFakeLayerId(parentId, digest);
            Console.log("正在下载第{}层{}，预计消耗{}MB流量", index + 1, fakeLayerId, layer.getSize() / 1024 / 1024);
            downloadAndExtractLayer(digest, layerDir + "/" + fakeLayerId + ".tar");
            parentId = fakeLayerId;
        }
        Console.log("所有分层下载完成，准备开始打包(由于服务端使用了gzip对数据进行压缩，实际下载的分层文件会比较大)");
    }

    /**
     * 生成manifest.json文件，是镜像文件中最重要的文件
     *
     * @author 张涵哲
     * @since 2024-06-13 18:22:36
     */
    public static void createManifestJson(DockerImage dockerImage)  {
        JSONArray content = new JSONArray();
        JSONObject configObject = new JSONObject();
        configObject.putOpt("Config", dockerImage.getDigest().substring(7) + ".json");
        configObject.putOpt("RepoTags", new JSONArray().put(DockerProp.repository + "/" + DockerProp.img + ":" + DockerProp.tag));
        JSONArray layerArray = new JSONArray();
        String parentId = "";
        for (Layer layer : dockerImage.getLayers()) {
            String digest = layer.getDigest();
            String fakeLayerId = generateFakeLayerId(parentId, digest);
            layerArray.put("layers/" + fakeLayerId + ".tar");
            parentId = fakeLayerId;
        }
        configObject.putOpt("Layers", layerArray);
        content.add(configObject);

        String path = DockerProp.workspace.getAbsolutePath() + "/" + "manifest.json";
        FileUtil.writeString(JSONUtil.toJsonPrettyStr(content), path, StandardCharsets.UTF_8);
    }

    /**
     * 生成repositories文件，有了这个文件在执行 docker load -i 的时候可以识别到[image:tag]
     *
     * @author 张涵哲
     * @since 2024-06-13 22:33:56
     */
    public static void createRepositoriesFile() {
        String imageName = DockerProp.getImgTag();
        JSONObject content = new JSONObject();
        content.putOpt(imageName, new JSONObject().putOnce(DockerProp.tag, "latest"));

        String path = DockerProp.workspace.getAbsolutePath() + "/repositories";
        FileUtil.writeString(JSONUtil.toJsonPrettyStr(content), path, StandardCharsets.UTF_8);
    }

    /**
     * 将准备好的文件打包为最终的镜像tar文件
     *
     * @author 张涵哲
     * @since 2024-06-13 22:35:49
     */
    public static void createTarFile() {
        File tarFile = new File(DockerProp.getImgTag() + ".tar");
        if (tarFile.exists()) {
            tarFile.delete();
        }
        try (FileOutputStream fos = new FileOutputStream(tarFile); TarArchiveOutputStream taros = new TarArchiveOutputStream(fos)) {
            for (File file : ObjUtil.defaultIfNull(DockerProp.workspace.listFiles(), new File[0])) {
                addFilesToTar(taros, file, "");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        Console.log("镜像打包完成：{}", tarFile.getAbsolutePath());
    }

    /**
     * 递归遍历该目录下所有文件，添加到taros中并打包
     *
     * @author 张涵哲
     * @since 2024-06-13 23:03:49
     * @param taros  最终输入的tar文件
     * @param file   当前文件/目录
     * @param parent 上一层目录
     */
    private static void addFilesToTar(TarArchiveOutputStream taros, File file, String parent) throws IOException {
        String entryName = parent + file.getName();
        TarArchiveEntry tarEntry = new TarArchiveEntry(file, entryName);
        taros.putArchiveEntry(tarEntry);

        if (file.isFile()) {
            try (FileInputStream fis = new FileInputStream(file)) {
                IoUtil.copy(fis, taros);
            }
            taros.closeArchiveEntry();
        } else if (file.isDirectory()) {
            taros.closeArchiveEntry();
            File[] children = file.listFiles();
            if (ArrayUtil.isNotEmpty(children)) {
                for (File child : children) {
                    addFilesToTar(taros, child, entryName + "/");
                }
            }
        }
    }

    /**
     * 生成LayerId
     *
     * @author 张涵哲
     * @since 2024-06-13 18:01:58
     */
    private static String generateFakeLayerId(String parentId, String digest) {
        MessageDigest sha256 = null;
        try {
            sha256 = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        sha256.update((parentId + "\n" + digest + "\n").getBytes(StandardCharsets.UTF_8));
        byte[] hash = sha256.digest();
        StringBuilder hexString = new StringBuilder();
        for (byte b : hash) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }

    /**
     * 下载Layer为tar的方法
     *
     * @author 张涵哲
     * @since 2024-06-13 18:02:14
     */
    private static void downloadAndExtractLayer(String digest, String outputPath) {
        String mediaType = "application/vnd.docker.distribution.manifest.list.v2+json";
        String requestUrl = "https://" + DockerProp.registry + "/v2/" + DockerProp.repository + "/" + DockerProp.img + "/blobs/" + digest;
        try (Response response = get(requestUrl, mediaType)) {
            InputStream inputStream = response.body().byteStream();
            GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
            FileUtil.writeFromStream(gzipInputStream, outputPath);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}
